﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Confuser.Renamer.Properties;

namespace Confuser.Renamer.BAML {
	/// <summary>
	/// This is the parser for property paths that are used in WPF to reference properties.
	/// </summary>
	/// <remarks>
	/// This parser is based on <see cref="MS.Internal.Data.PathParser"/>. See the reference source for the original
	/// implementation: https://referencesource.microsoft.com/#PresentationFramework/src/Framework/MS/Internal/Data/PathParser.cs,408574f0013d222e
	/// </remarks>
	internal sealed class PropertyPathParser {
		private enum State { Init, DrillIn, Prop, Done }

		internal string Error { get; private set; }

		private void SetError(string message, params string[] args) =>
			Error = string.Format(CultureInfo.CurrentUICulture, message, args);

		// Each level of the path consists of
		//      a property or indexer:
		//                  .propname
		//                  /propname
		//                  [index]
		//                  /[index]
		//          (The . or / is optional in the very first level.)
		// The parser is a finite-state machine with two states corresponding
		// to the two-character lookahead above, plus two more states for the begining
		// and end.  The state transistions are done explicitly in the code below.
		//
		// The parser returns a 0-length array if it finds a syntax error.
		// It sets the Error property, so the caller can find out what happened.

		public SourceValueInfo[] Parse(string path) {
			_path = (path != null) ? path.Trim() : String.Empty;
			_n = _path.Length;

			if (_n == 0) {
				// When no path string is specified, use value directly and do not drill-in. (same as Path=".")
				// ClrBindingWorker needs this information to tell XmlBindingWorker about collectionMode.
				return new SourceValueInfo[] { new SourceValueInfo(SourceValueType.Direct, DrillIn.Never, (string)null) };
			}

			_index = 0;
			_drillIn = DrillIn.IfNeeded;

			_al.Clear();
			Error = null;
			_state = State.Init;

			while (_state != State.Done) {
				char c = (_index < _n) ? _path[_index] : NullChar;
				if (Char.IsWhiteSpace(c)) {
					++_index;
					continue;
				}

				switch (_state) {
					case State.Init:
						switch (c) {
							case '/':
							case '.':
							case NullChar:
								_state = State.DrillIn;
								break;
							default:
								_state = State.Prop;
								break;
						}
						break;

					case State.DrillIn:
						switch (c) {
							case '/':
								_drillIn = DrillIn.Always;
								++_index;
								break;
							case '.':
								_drillIn = DrillIn.Never;
								++_index;
								break;
							case '[':
							case NullChar:
								break;
							default:
								SetError(Resources.InvalidPathSyntax, _path.Substring(0, _index), _path.Substring(_index));
								return EmptyInfo;
						}
						_state = State.Prop;
						break;

					case State.Prop:
						bool isIndexer = false;
						switch (c) {
							case '[':
								isIndexer = true;
								break;
							default:
								break;
						}

						if (isIndexer)
							AddIndexer();
						else
							AddProperty();

						break;
				}
			}


			SourceValueInfo[] result;

			if (Error == null) {
				result = new SourceValueInfo[_al.Count];
				_al.CopyTo(result);
			}
			else {
				result = EmptyInfo;
			}

			return result;
		}

		void AddProperty() {
			int start = _index;
			int level = 0;

			// include leading dots in the path (for XLinq)
			while (_index < _n && _path[_index] == '.')
				++_index;

			while (_index < _n && (level > 0 || SpecialChars.IndexOf(_path[_index]) < 0)) {
				if (_path[_index] == '(')
					++level;
				else if (_path[_index] == ')')
					--level;

				++_index;
			}

			if (level > 0) {
				SetError(Resources.UnmatchedParen, _path.Substring(start));
				return;
			}

			if (level < 0) {
				SetError(Resources.UnmatchedParen, _path.Substring(0, _index));
				return;
			}

			string name = _path.Substring(start, _index - start).Trim();

			SourceValueInfo info = (name.Length > 0)
				? new SourceValueInfo(SourceValueType.Property, _drillIn, name)
				: new SourceValueInfo(SourceValueType.Direct, _drillIn, (string)null);

			_al.Add(info);

			StartNewLevel();
		}


		enum IndexerState { BeginParam, ParenString, ValueString, Done }

		void AddIndexer() {
			// indexer args are parsed by a (sub-) state machine with four
			// states.  The string is a comma-separated list of params, each
			// of which has two parts:  a "paren string" and a "value string"
			// (both parts are optional).  The character ^ can be used to
			// escape any of the special characters:  comma, parens, ], ^,
			// and white space.

			int start = ++_index;       // skip over initial [
			int level = 1;              // level of nested []

			bool escaped = false;       // true if current char is escaped
			bool trimRight = false;     // true if value string has trailing white space

			StringBuilder parenStringBuilder = new StringBuilder();
			StringBuilder valueStringBuilder = new StringBuilder();

			List<IndexerParamInfo> paramList = new List<IndexerParamInfo>(0);

			IndexerState state = IndexerState.BeginParam;
			while (state != IndexerState.Done) {
				if (_index >= _n) {
					SetError(Resources.UnmatchedBracket, _path.Substring(start - 1));
					return;
				}

				Char c = _path[_index++];

				// handle the escape character - set the flag for the next character
				if (c == EscapeChar && !escaped) {
					escaped = true;
					continue;
				}

				switch (state) {
					case IndexerState.BeginParam:   // look for optional (...)
						if (escaped) {
							// no '(', go parse the value
							state = IndexerState.ValueString;
							goto case IndexerState.ValueString;
						}
						else if (c == '(') {
							// '(' introduces optional paren string
							state = IndexerState.ParenString;
						}
						else if (Char.IsWhiteSpace(c)) {
							// ignore leading white space
						}
						else {
							// no '(', go parse the value
							state = IndexerState.ValueString;
							goto case IndexerState.ValueString;
						}
						break;

					case IndexerState.ParenString:  // parse (...)
						if (escaped) {
							// add an escaped character without question
							parenStringBuilder.Append(c);
						}
						else if (c == ')') {
							// end of (...), start to parse value
							state = IndexerState.ValueString;
						}
						else {
							// add normal characters inside (...)
							parenStringBuilder.Append(c);
						}
						break;

					case IndexerState.ValueString:  // parse value
						if (escaped) {
							// add an escaped character without question
							valueStringBuilder.Append(c);
							trimRight = false;
						}
						else if (level > 1) {
							// inside nested [], add characters without question
							valueStringBuilder.Append(c);
							trimRight = false;

							if (c == ']') {
								--level;
							}
						}
						else if (Char.IsWhiteSpace(c)) {
							// add white space, but trim it later if it's trailing
							valueStringBuilder.Append(c);
							trimRight = true;
						}
						else if (c == ',' || c == ']') {
							// end of current paramater - assemble the two parts
							string parenString = parenStringBuilder.ToString();
							string valueString = valueStringBuilder.ToString();
							if (trimRight) {
								valueString = valueString.TrimEnd();
							}

							// add the parts to the final result
							paramList.Add(new IndexerParamInfo(parenString, valueString));

							// reset for the next parameter
							parenStringBuilder.Length = 0;
							valueStringBuilder.Length = 0;
							trimRight = false;

							// after ',' parse next parameter;  after ']' we're done
							state = (c == ']') ? IndexerState.Done : IndexerState.BeginParam;
						}
						else {
							// add normal characters
							valueStringBuilder.Append(c);
							trimRight = false;

							// keep track of nested []
							if (c == '[') {
								++level;
							}
						}
						break;
				}

				// after processing each character, clear the escape flag
				escaped = false;
			}

			// assemble the final result
			SourceValueInfo info = new SourceValueInfo(
										SourceValueType.Indexer,
										_drillIn, paramList);
			_al.Add(info);

			StartNewLevel();
		}

		void StartNewLevel() {
			_state = (_index < _n) ? State.DrillIn : State.Done;
			_drillIn = DrillIn.Never;
		}

		State _state;
		string _path;
		int _index;
		int _n;
		DrillIn _drillIn;
		ArrayList _al = new ArrayList();
		const char NullChar = Char.MinValue;
		const char EscapeChar = '^';
		static SourceValueInfo[] EmptyInfo = new SourceValueInfo[0];
		static string SpecialChars = @"./[]";
	}
}
